#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "pear_curl.h"


static int pear_curl_global_init = 0;


// fuqiyang
//
static void pear_dump(const char *text, FILE *stream, unsigned char *ptr, size_t size)
{
   size_t i;
   size_t c;
   unsigned int width = 0x10;

   fprintf(stream, "%s, %10.10ld bytes (0x%8.8lx)\n", text, (long)size, (long)size);

   for(i=0; i<size; i+= width) {

       fprintf(stream, "%4.4lx: ", (long)i);

       /* show hex to the left */
       for(c = 0; c < width; c++) {
           if(i+c < size)
               fprintf(stream, "%02x ", ptr[i+c]);
           else
               fputs("   ", stream);
       }

       /* show data on the right */
       for(c = 0; (c < width) && (i+c < size); c++) {
           char x = (ptr[i+c] >= 0x20 && ptr[i+c] < 0x80) ? ptr[i+c] : '.';
           fputc(x, stream);
       }

       fputc('\n', stream); /* newline */
   }
}


// fuqiyang
//
static int pear_curl_trace(CURL *handle, curl_infotype type, char *data, size_t size, void *userp)
{
   const char *text;

   (void)handle; /* prevent compiler warning */

   (void)userp;

   switch (type) {
       case CURLINFO_TEXT:
           fprintf(stderr, "== Info: %s", data);
       default: /* in case a new one is introduced to shock us */
           return 0;

       case CURLINFO_HEADER_OUT:
           text = "=> Send header";
           break;
       case CURLINFO_DATA_OUT:
           text = "=> Send data";
           break;
       case CURLINFO_SSL_DATA_OUT:
           text = "=> Send SSL data";
           break;
       case CURLINFO_HEADER_IN:
           text = "<= Recv header";
           break;
       case CURLINFO_DATA_IN:
           text = "<= Recv data";
           break;
       case CURLINFO_SSL_DATA_IN:
           text = "<= Recv SSL data";
           break;
   }

   pear_dump(text, stderr, (unsigned char *)data, size);
   return 0;
}


struct Pear_Curl_Buffer {

    char *buffer;

    size_t size;
};


static size_t pear_curl_writecb(void *buf, size_t size, size_t nmemb, void *recv)
{
    size_t realsize = size * nmemb;

    struct Pear_Curl_Buffer *chunk = (struct Pear_Curl_Buffer *)recv;

    chunk->buffer = realloc(chunk->buffer, chunk->size + realsize + 1);

    if (NULL == chunk->buffer) {

        chunk->size = 0;

        return 0;
    }

    memcpy(&(chunk->buffer[chunk->size]), buf, realsize);

    chunk->size += realsize;

    // chunk->buffer[chunk->size] = 0;

    return realsize;
}



// fuqiyang  2017-03-27
//
long pear_curl_cmd(char *method, char *url,  char *par, char *header[], char *body, long body_size, char *recvbuf, int recvbuf_len, int timeout)
{
    if (!pear_curl_global_init) {
      curl_global_init(CURL_GLOBAL_ALL);
      pear_curl_global_init = 1;
    }

    CURL *curl_handle = curl_easy_init();

    /// for debug
    ///
    //curl_easy_setopt(curl_handle, CURLOPT_DEBUGFUNCTION, pear_curl_trace);
    //curl_easy_setopt(curl_handle, CURLOPT_VERBOSE, 1L);

    /// add header
    ///
    struct curl_slist *curl_headers = NULL;
    
    if (header != NULL) {

        for (; *header; header++) {
            curl_headers = curl_slist_append(curl_headers, *header);
        }
        curl_easy_setopt(curl_handle, CURLOPT_HTTPHEADER, curl_headers);
    }

    /// set callback function
    ///
    struct Pear_Curl_Buffer chunk;

    chunk.buffer = malloc(1);

    chunk.size = 0;

    curl_easy_setopt(curl_handle, CURLOPT_WRITEFUNCTION, pear_curl_writecb);
    curl_easy_setopt(curl_handle, CURLOPT_WRITEDATA, &chunk);

    /// POST and GET, decide if there is body data
    ///
    if (strcmp(method, "POST") == 0) {

        curl_easy_setopt(curl_handle, CURLOPT_POST, 1L);

        curl_easy_setopt(curl_handle, CURLOPT_POSTFIELDSIZE, body_size);

        curl_easy_setopt(curl_handle, CURLOPT_POSTFIELDS, body);

    } else if (strcmp(method, "GET") == 0) {

    } else { return -1; }

    /// other options
    ///
    curl_easy_setopt(curl_handle, CURLOPT_NOBODY, 0L);
    curl_easy_setopt(curl_handle, CURLOPT_TIMEOUT, timeout);
    curl_easy_setopt(curl_handle, CURLOPT_URL, url);
    //curl_easy_setopt(curl_handle, CURLOPT_USERAGENT, "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36");
    curl_easy_setopt(curl_handle, CURLOPT_USERAGENT, "curl/7.50");
    curl_easy_setopt(curl_handle, CURLOPT_SSL_VERIFYPEER, 0);
	curl_easy_setopt(curl_handle, CURLOPT_NOPROGRESS, 1L);

    /// perform the url
    ///
    curl_easy_perform(curl_handle);

    long retcode;

    CURLcode res = curl_easy_getinfo(curl_handle, CURLINFO_HTTP_CODE, &retcode);

    /// copy data
    ///
    int copy_bytes = (recvbuf_len <= chunk.size)? recvbuf_len:chunk.size;
    memcpy(recvbuf, chunk.buffer, copy_bytes);
    recvbuf[copy_bytes] = 0;

    /// clean up
    ///
    free(chunk.buffer);

    curl_slist_free_all(curl_headers);

    curl_easy_cleanup(curl_handle);

    // curl_global_cleanup();

    return retcode;
}



long pear_curl_cmd_debug(char *method, char *url,  char *par, char *header[], char *body, long body_size, char *recvbuf, int recvbuf_len, int timeout)
{
    if (!pear_curl_global_init) {
      curl_global_init(CURL_GLOBAL_ALL);
      pear_curl_global_init = 1;
    }

    CURL *curl_handle = curl_easy_init();

    /// for debug
    ///
    curl_easy_setopt(curl_handle, CURLOPT_DEBUGFUNCTION, pear_curl_trace);
    curl_easy_setopt(curl_handle, CURLOPT_VERBOSE, 1L);

    /// add header
    ///
    struct curl_slist *curl_headers = NULL;
    
    if (header != NULL) {

        for (; *header; header++) {
            curl_headers = curl_slist_append(curl_headers, *header);
        }
        curl_easy_setopt(curl_handle, CURLOPT_HTTPHEADER, curl_headers);
    }

    /// set callback function
    ///
    struct Pear_Curl_Buffer chunk;

    chunk.buffer = malloc(1);

    chunk.size = 0;

    curl_easy_setopt(curl_handle, CURLOPT_WRITEFUNCTION, pear_curl_writecb);
    curl_easy_setopt(curl_handle, CURLOPT_WRITEDATA, &chunk);

    /// POST and GET, decide if there is body data
    ///
    if (strcmp(method, "POST") == 0) {

        curl_easy_setopt(curl_handle, CURLOPT_POST, 1L);

        curl_easy_setopt(curl_handle, CURLOPT_POSTFIELDSIZE, body_size);

        curl_easy_setopt(curl_handle, CURLOPT_POSTFIELDS, body);

    } else if (strcmp(method, "GET") == 0) {

    } else { return -1; }

    /// other options
    ///
    curl_easy_setopt(curl_handle, CURLOPT_NOBODY, 0L);
    curl_easy_setopt(curl_handle, CURLOPT_TIMEOUT, timeout);
    curl_easy_setopt(curl_handle, CURLOPT_URL, url);
    //curl_easy_setopt(curl_handle, CURLOPT_USERAGENT, "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36");
    curl_easy_setopt(curl_handle, CURLOPT_USERAGENT, "curl/7.50");
    curl_easy_setopt(curl_handle, CURLOPT_SSL_VERIFYPEER, 0);

    /// perform the url
    ///
    curl_easy_perform(curl_handle);

    long retcode;

    CURLcode res = curl_easy_getinfo(curl_handle, CURLINFO_HTTP_CODE, &retcode);

    /// copy data
    ///
    int copy_bytes = (recvbuf_len <= chunk.size)? recvbuf_len:chunk.size;
    memcpy(recvbuf, chunk.buffer, copy_bytes);
    recvbuf[copy_bytes] = 0;

    /// clean up
    ///
    free(chunk.buffer);

    curl_slist_free_all(curl_headers);

    curl_easy_cleanup(curl_handle);

    // curl_global_cleanup();

    return retcode;
}



static size_t pear_curl_writecb_no(void *buf, size_t size, size_t nmemb, void *recv)
{
    size_t realsize = size * nmemb;

    return realsize;
}



// fuqiyang  2017-03-27
//
long pear_curl_cmd_limit_speed(char *method, char *url, char *header[], curl_off_t recv_speed_bytes, int timeout)
{
    if (!pear_curl_global_init) {
      curl_global_init(CURL_GLOBAL_ALL);
      pear_curl_global_init = 1;
    }

    CURL *curl_handle = curl_easy_init();

    /// add header
    ///
    struct curl_slist *curl_headers = NULL;
    
    if (header != NULL) {

        for (; *header; header++) {
            curl_headers = curl_slist_append(curl_headers, *header);
        }
        curl_easy_setopt(curl_handle, CURLOPT_HTTPHEADER, curl_headers);
    }

    /// set callback function
    ///
    curl_easy_setopt(curl_handle, CURLOPT_WRITEFUNCTION, pear_curl_writecb_no);
    curl_easy_setopt(curl_handle, CURLOPT_WRITEDATA, 0);

    /// METHOD
    ///
    if (strcmp(method, "GET") == 0) {

    } else { return -1; }

    /// speed and progress bar
    curl_easy_setopt(curl_handle, CURLOPT_MAX_RECV_SPEED_LARGE, recv_speed_bytes);
     curl_easy_setopt(curl_handle, CURLOPT_NOPROGRESS, 1L);

    /// speed limit(30 second)
    ///
    curl_easy_setopt(curl_handle, CURLOPT_LOW_SPEED_TIME, 30L);
    if (recv_speed_bytes)
        curl_easy_setopt(curl_handle, CURLOPT_LOW_SPEED_LIMIT, recv_speed_bytes >> 1);

    /// other options
    ///
    curl_easy_setopt(curl_handle, CURLOPT_NOBODY, 0L);
    curl_easy_setopt(curl_handle, CURLOPT_TIMEOUT, timeout);
    curl_easy_setopt(curl_handle, CURLOPT_URL, url);
    curl_easy_setopt(curl_handle, CURLOPT_FOLLOWLOCATION, 1L);
    //curl_easy_setopt(curl_handle, CURLOPT_USERAGENT, "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36");
    curl_easy_setopt(curl_handle, CURLOPT_USERAGENT, "curl/7.50");
    curl_easy_setopt(curl_handle, CURLOPT_SSL_VERIFYPEER, 0);

    /// perform the url
    ///
    curl_easy_perform(curl_handle);

    long retcode;

    CURLcode res = curl_easy_getinfo(curl_handle, CURLINFO_HTTP_CODE, &retcode);

    /// clean up
    ///
    curl_slist_free_all(curl_headers); 
    curl_easy_cleanup(curl_handle);

    // curl_global_cleanup();

    return retcode;
}



// 3. ----------------  download file ------------------------------------
//
enum fcurl_type_e {

  CFTYPE_NONE = 0,
  CFTYPE_FILE = 1,
  CFTYPE_CURL = 2
};
 
typedef struct fcurl_data
{
  enum fcurl_type_e type;         /* type of handle */ 
  union {
    CURL *curl;
    FILE *file;
  } handle;                       /* handle */ 

  struct curl_slist *headers;   /* header */

  char *buffer;                   /* buffer to store cached data*/ 
  size_t buffer_len;              /* currently allocated buffers length */ 
  size_t buffer_pos;              /* end of data in buffer*/ 
  int still_running;              /* Is background url fetch still in progress */ 

}URL_FILE;
 
// we use a global one for convenience
//
static CURLM *multi_handle;
 
// curl calls this routine to get more data 
//
static size_t write_callback(char *buffer, size_t size, size_t nitems, void *userp)
{
  char *newbuff;
  size_t rembuff;

  URL_FILE *url = (URL_FILE *)userp;
  size *= nitems;

  rembuff = url->buffer_len - url->buffer_pos; /* remaining space in buffer */ 

  if(size > rembuff) {

    /* not enough space in buffer */ 
    newbuff = realloc(url->buffer, url->buffer_len + (size - rembuff));
    if(newbuff == NULL) {
      fprintf(stderr, "callback buffer grow failed\n");
      size = rembuff;

    } else {
      /* realloc succeeded increase buffer size*/ 
      url->buffer_len += size - rembuff;
      url->buffer = newbuff;
    }
  }

  memcpy(&url->buffer[url->buffer_pos], buffer, size);
  url->buffer_pos += size;

  return size;
}
 

// use to attempt to fill the read buffer up to requested number of bytes
//
static int fill_buffer(URL_FILE *file, size_t want)
{
  fd_set fdread;
  fd_set fdwrite;
  fd_set fdexcep;
  struct timeval timeout;
  int rc;
  CURLMcode mc; /* curl_multi_fdset() return code */ 

  /* only attempt to fill buffer if transactions still running and buffer
  * doesn't exceed required size already
  */ 
  if((!file->still_running) || (file->buffer_pos > want))
  return 0;

  /* attempt to fill buffer */ 
  do {
    int maxfd = -1;
    long curl_timeo = -1;

    FD_ZERO(&fdread);
    FD_ZERO(&fdwrite);
    FD_ZERO(&fdexcep);

    /* set a suitable timeout to fail on */ 
    timeout.tv_sec = 60; /* 1 minute */ 
    timeout.tv_usec = 0;

    curl_multi_timeout(multi_handle, &curl_timeo);
    if(curl_timeo >= 0) {
      timeout.tv_sec = curl_timeo / 1000;
      if(timeout.tv_sec > 1)
        timeout.tv_sec = 1;
      else
        timeout.tv_usec = (curl_timeo % 1000) * 1000;
    }

    /* get file descriptors from the transfers */ 
    mc = curl_multi_fdset(multi_handle, &fdread, &fdwrite, &fdexcep, &maxfd);

    if(mc != CURLM_OK) {
      fprintf(stderr, "curl_multi_fdset() failed, code %d.\n", mc);
      break;
    }

    /* On success the value of maxfd is guaranteed to be >= -1. We call
    select(maxfd + 1, ...); specially in case of (maxfd == -1) there are
    no fds ready yet so we call select(0, ...) --or Sleep() on Windows--
    to sleep 100ms, which is the minimum suggested value in the
    curl_multi_fdset() doc. */ 

    if(maxfd == -1) {
      #ifdef _WIN32
        Sleep(100);
        rc = 0;
      #else
        /* Portable sleep for platforms other than Windows. */ 
        struct timeval wait = { 0, 100 * 1000 }; /* 100ms */ 
        rc = select(0, NULL, NULL, NULL, &wait);
      #endif
    }
    else {
      /* Note that on some platforms 'timeout' may be modified by select().
       If you need access to the original value save a copy beforehand. */ 
      rc = select(maxfd + 1, &fdread, &fdwrite, &fdexcep, &timeout);
    }

    switch(rc) {
      case -1:
        /* select error */ 
        break;

      case 0:
      default:
        /* timeout or readable/writable sockets */ 
        curl_multi_perform(multi_handle, &file->still_running);
        break;
    }

  } while(file->still_running && (file->buffer_pos < want));

  return 1;
}

 
// use to remove want bytes from the front of a files buffer
//
static int use_buffer(URL_FILE *file, size_t want)
{
  /* sort out buffer */ 
  if((file->buffer_pos - want) <= 0) {
    /* ditch buffer - write will recreate */ 
    free(file->buffer);
    file->buffer = NULL;
    file->buffer_pos = 0;
    file->buffer_len = 0;
  }
  else {
    /* move rest down make it available for later */ 
    memmove(file->buffer,
        &file->buffer[want],
        (file->buffer_pos - want));

    file->buffer_pos -= want;
  }
  return 0;
}

 
//
//
static URL_FILE *url_fopen(const char *url, const char *operation, const char *method, 
                    char *header[], char *body, long body_len, long connect_timeout, long total_timeout)
{
  /* this code could check for URLs or types in the 'url' and
  basically use the real fopen() for standard files */ 

  URL_FILE *file;
  (void)operation;

  file = calloc(1, sizeof(URL_FILE));
  if(!file)
    return NULL;

  file->handle.file = fopen(url, operation);
  if(file->handle.file)
    file->type = CFTYPE_FILE; /* marked as URL */ 
  else {
    file->type = CFTYPE_CURL; /* marked as URL */ 
    file->handle.curl = curl_easy_init();

    // add header
    //
    file->headers = NULL;
    
    if (header != NULL) {

      for (; *header; header++) { file->headers = curl_slist_append(file->headers, *header); }

      curl_easy_setopt(file->handle.curl, CURLOPT_HTTPHEADER, file->headers);
    }

    // options
    //
    curl_easy_setopt(file->handle.curl, CURLOPT_URL, url);
    curl_easy_setopt(file->handle.curl, CURLOPT_WRITEDATA, file);
    curl_easy_setopt(file->handle.curl, CURLOPT_WRITEFUNCTION, write_callback);
    curl_easy_setopt(file->handle.curl, CURLOPT_VERBOSE, 0L);
    curl_easy_setopt(file->handle.curl, CURLOPT_NOPROGRESS, 1L);
    //curl_easy_setopt(file->handle.curl, CURLOPT_USERAGENT, "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36");
    curl_easy_setopt(file->handle.curl, CURLOPT_USERAGENT, "curl/7.50");

    // curl -L
    //
    curl_easy_setopt(file->handle.curl, CURLOPT_FOLLOWLOCATION, 1L);

    // curl -k or curl --insecure
    //
    curl_easy_setopt(file->handle.curl, CURLOPT_SSL_VERIFYPEER, 0);

    // curl --connect-timeout 50
    //
    curl_easy_setopt(file->handle.curl, CURLOPT_CONNECTTIMEOUT, connect_timeout);

    // curl -m 600
    //
    curl_easy_setopt(file->handle.curl, CURLOPT_TIMEOUT, total_timeout);


    // body
    //
    if (strcmp(method, "GET") == 0) {

    } else if (strcmp(method, "POST") == 0) {

            curl_easy_setopt(file->handle.curl, CURLOPT_POST, 1L);

            curl_easy_setopt(file->handle.curl, CURLOPT_POSTFIELDSIZE, body_len);

      curl_easy_setopt(file->handle.curl, CURLOPT_POSTFIELDS, body);

    } else { exit(-1); }

    if(!multi_handle)
      multi_handle = curl_multi_init();

    curl_multi_add_handle(multi_handle, file->handle.curl);

    /* lets start the fetch */ 
    curl_multi_perform(multi_handle, &file->still_running);

    if((file->buffer_pos == 0) && (!file->still_running)) {

      /* if still_running is 0 now, we should return NULL */ 

      /* make sure the easy handle is not in the multi handle anymore */ 
      curl_multi_remove_handle(multi_handle, file->handle.curl);

      /* cleanup */ 
      curl_easy_cleanup(file->handle.curl);

      curl_slist_free_all(file->headers);

      free(file);

      file = NULL;
    }
  }
  return file;
}

 
//
//
static size_t url_fread(void *ptr, size_t size, size_t nmemb, URL_FILE *file)
{
  size_t want;

  switch(file->type) {
    case CFTYPE_FILE:
      want = fread(ptr, size, nmemb, file->handle.file);
      break;

    case CFTYPE_CURL:
      want = nmemb * size;

      fill_buffer(file, want);

      /* check if there's data in the buffer - if not fill_buffer()
      * either errored or EOF */ 
      if(!file->buffer_pos) { return 0; }

      /* ensure only available data is considered */ 
      if(file->buffer_pos < want) { want = file->buffer_pos; }

      /* xfer data to caller */ 
      memcpy(ptr, file->buffer, want);

      use_buffer(file, want);

      want = want / size;     /* number of items */ 
      break;

    default: /* unknown or supported type - oh dear */ 
      want = 0;
      // errno = EBADF;
      break;

  }
  return want;
}


//
//
static int url_fclose(URL_FILE *file)
{
  int ret = 0;/* default is good return */ 

  switch(file->type) {
    case CFTYPE_FILE:
      ret = fclose(file->handle.file); /* passthrough */ 
      break;

    case CFTYPE_CURL:
      /* make sure the easy handle is not in the multi handle anymore */ 
      curl_multi_remove_handle(multi_handle, file->handle.curl);

      /* cleanup */ 
      curl_easy_cleanup(file->handle.curl);

      curl_slist_free_all(file->headers);

      break;

    default: /* unknown or supported type - oh dear */ 
      ret = EOF;
      //errno = EBADF;
      break;
  }

  free(file->buffer);/* free any allocated buffer space */ 
  free(file);

  return ret;
}



void pear_download_buffer(const char *method, const char *url, char *header[], char *body, long body_len, int connect_timeout, int total_timeout, char *buffer, long buffer_len)
{
  // fetch data from url
  //
  URL_FILE *handle = url_fopen(url, "r", method, header, body, body_len, connect_timeout, total_timeout);

  if (!handle) { return ; }

  // fetch data from url to store in buffer
  //
  memset(buffer, 0, buffer_len);

  size_t nread_already = 0;
  size_t nread;

  do {
        nread = url_fread(buffer + nread_already, 1, buffer_len - nread_already, handle);

    nread_already += nread;

  }while(nread);

  url_fclose(handle);

  //fprintf(stderr, "buffer: %ld\n", nread_already);
}


size_t pear_download_file(const char *method, const char *url, char *header[], char *body, long body_len, long connect_timeout, long total_timeout, const char *path)
{
  // fetch data from url
  //
  URL_FILE *handle = url_fopen(url, "r", method, header, body, body_len, connect_timeout, total_timeout);
  if (!handle) { return 0; }

  // store in local file
  //
  FILE *outf = fopen(path, "wb+");
  if (!outf) { 
    url_fclose(handle);
    return 0; 
  }

  // fetch data from url to store in local file
  //
  size_t nread;
  size_t nread_already = 0;
  char buffer[4096];
  do {
    nread = url_fread(buffer, 1, sizeof(buffer), handle);

    nread_already += nread;

    fwrite(buffer, 1, nread, outf);

  } while(nread > 0);

  url_fclose(handle);

  fclose(outf);

  fprintf(stderr, "url(%s), filepath(%s), filesize(%ld)\n", url, path, nread_already);

  return nread_already;
}


